package cn.org.donald.framework.aspect;

import cn.org.donald.framework.config.ServiceThreadPool;
import cn.org.donald.framework.constant.HttpStatus;
import cn.org.donald.framework.constant.InfoConstant;
import cn.org.donald.framework.handler.ServiceException;
import cn.org.donald.framework.pojo.vo.HttpResult;
import cn.org.donald.framework.pojo.vo.TableResult;
import cn.org.donald.framework.service.SysLoginService;
import cn.org.donald.framework.util.ApplicationContextUtil;
import cn.org.donald.framework.util.RequestUtil;
import cn.org.donald.pojo.entity.system.LogOperator;
import cn.org.donald.service.system.LogOperatorService;
import com.alibaba.fastjson.JSON;
import io.swagger.annotations.ApiOperation;
import org.apache.dubbo.config.annotation.Reference;
import org.apache.dubbo.rpc.RpcException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.CodeSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;


/**
 * Global controller aspect cut
 * Writer operation logs
 *
 * @author Donald
 * @since 2020-12-29 20:35:40
 */
@Component
@Aspect
@SuppressWarnings("unchecked")
public class GlobalControllerAspect {

    private static Logger logger = LoggerFactory.getLogger(GlobalControllerAspect.class);

    @Reference(version = "1.0.0", group = "donald")
    private LogOperatorService logOperatorService;

    @Reference(version = "1.0.0", group = "donald")
    private SysLoginService sysLoginService;

    @Resource
    private ServiceThreadPool serviceThreadPool;

    @Value("${spring.application.name}")
    private String application;

    private Long startTimestamp = 0L;

    private HttpServletRequest httpServletRequest = null;

    private String title = null;

    private String argument;

    private String info;

    private Integer code;

    private String stackTrace;


    @Pointcut("execution(* cn.org.donald..*Controller.*(..))")
    public void logJoinPoint() {
    }

    @Before("logJoinPoint()")
    public void before(JoinPoint joinPoint) {
        startTimestamp = System.currentTimeMillis();
        httpServletRequest = getHttpServletRequest();
        // 验证BindingResult
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof BindingResult){
                BindingResult bindingResult = (BindingResult) arg;
                if (bindingResult.hasErrors()){
                    throw new ServiceException(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
                }
            }
        }
    }

    @After(value = "logJoinPoint()")
    public void after(JoinPoint joinPoint) {
        String targetName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        Class<?> targetClass = null;
        try {
            targetClass = Class.forName(targetName);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        Assert.notNull(targetClass, "target class must not be null!");
        Method[] methods = targetClass.getMethods();
        Class<?>[] clazzArray;
        this.title = "";
        for (Method method : methods) {
            if (methodName.equals(method.getName())) {
                clazzArray = method.getParameterTypes();
                if (clazzArray.length == args.length) {
                    ApiOperation annotation = method.getAnnotation(ApiOperation.class);
                    if (annotation != null) {
                        title = annotation.value();
                    }
                    argument = getArguments(joinPoint);
                }
            }
        }
    }


    /**
     * write all request into database except system api
     * @param joinPoint
     * @param ret
     */
    @AfterReturning(returning = "ret", pointcut = "logJoinPoint()")
    public void afterReturning(JoinPoint joinPoint, Object ret) {
        if (ret instanceof HttpResult) {
            info = ((HttpResult) ret).getInfo();
            code = ((HttpResult) ret).getCode();
        } else if (ret instanceof TableResult) {
            info = ((TableResult) ret).getInfo();
            code = ((TableResult) ret).getCode();
        }
        stackTrace = "";
        String servletPath = httpServletRequest.getServletPath();
        if (servletPath == null || "".equals(servletPath) || servletPath.startsWith("/system")) {
            return;
        }
        writeLog();
    }

    @AfterThrowing(pointcut = "logJoinPoint()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        e.printStackTrace();
        if (e instanceof ServiceException) {
            ServiceException e1 = (ServiceException) e;
            info = e1.getInfo();
            code = e1.getCode();
        } else if (e instanceof UnauthorizedException) {
            info = InfoConstant.FORBIDDEN;
            code = HttpStatus.FORBIDDEN;
        } else if (e instanceof UnauthenticatedException) {
            code = HttpStatus.UNAUTHORIZED;
            info = InfoConstant.UNAUTHORIZED;
        } else if (e instanceof UnknownAccountException) {
            info = InfoConstant.USER_NOT_EXIST;
            code = HttpStatus.UNAUTHORIZED;
        } else if (e instanceof IncorrectCredentialsException) {
            code = HttpStatus.UNAUTHORIZED;
            info = InfoConstant.PASSWORD_INCORRECT;
        } else if (e instanceof AuthenticationException) {
            info = InfoConstant.UNAUTHORIZED;
            code = HttpStatus.UNAUTHORIZED;
        } else if (e instanceof IllegalArgumentException) {
            info = InfoConstant.ILLEGAL_ARGUMENT;
            code = HttpStatus.INTERNAL_SERVER_ERROR;
        } else if (e instanceof NullPointerException) {
            info = InfoConstant.NULL_POINTER_EXCEPTION;
            code = HttpStatus.INTERNAL_SERVER_ERROR;
        } else if (e instanceof RpcException) {
            info = InfoConstant.RPC_EXCEPTION;
            code = HttpStatus.INTERNAL_SERVER_ERROR;
        } else {
            info = InfoConstant.INTERNAL_SERVER_ERROR;
            code = HttpStatus.INTERNAL_SERVER_ERROR;
        }
        StackTraceElement[] stackTraceElement = e.getStackTrace();
        StringBuilder stringBuilder = new StringBuilder("[");
        int i = 0;
        for (StackTraceElement stackTrace : stackTraceElement) {
            if (i > 5) {
                break;
            }
            stringBuilder.append("{").append(stackTrace.toString()).append("},");
            i++;
        }
        stringBuilder.delete(stringBuilder.length() - 1, stringBuilder.length()).append("]");
        this.stackTrace = stringBuilder.toString();
        writeLog();
        throw new ServiceException(code, info);
    }

    /**
     * Write log to database
     */
    private void writeLog() {
        logger.info("is writing operator log");
        Long operatorId = null;
        String operatorName = null;
        LogOperator logOperator = new LogOperator();
        String servletPath = httpServletRequest.getServletPath();
        logOperator.setPath(servletPath);
        try {
            operatorId = Long.parseLong(httpServletRequest.getHeader("userId"));
            String authorization = httpServletRequest.getHeader("authorization");
            operatorName = RequestUtil.getUserNameByAuthorization(authorization);
        } catch (Exception e) {
            logger.debug("用户没有请求头等信息");
        }
        logOperator.setOperatorId(operatorId);
        logOperator.setOperatorName(operatorName);
        logOperator.setTitle(this.title);
        logOperator.setArgument(this.argument);
        logOperator.setInfo(this.info);
        logOperator.setCode(this.code);
        logOperator.setStackTrace(stackTrace);
        String method = httpServletRequest.getMethod();
        logOperator.setRequestType(method);
        logOperator.setTimeConsume(System.currentTimeMillis() - startTimestamp);
        logOperator.setApplication(this.application);
        serviceThreadPool.execute(() -> {
            boolean b = false;
            try {
                b = logOperatorService.add(logOperator);
            } catch (Exception e) {
                e.getStackTrace();
            }
            if (b) {
                logger.debug("操作日志写入成功!");
            } else {
                logger.error("操作日志写入失败!");
            }
        });
    }


    /**
     * Get HttpServletRequest
     *
     * @return
     */
    private HttpServletRequest getHttpServletRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        Assert.notNull(requestAttributes, "request must not be null");
        return ((ServletRequestAttributes) requestAttributes).getRequest();
    }

    /**
     * Get argument
     *
     * @param joinPoint
     * @return
     */
    private String getArguments(JoinPoint joinPoint) {

        Object[] args = joinPoint.getArgs();
        String[] parameterNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        Map<String, Object> map = new HashMap<>(args.length);
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            if (arg instanceof Integer || arg instanceof String) {
                map.put(parameterNames[i], arg);
            } else {
                String json;
                try {
                    json = JSON.toJSONString(arg);
                } catch (Exception e) {
                    json = arg.toString();
                }
                map.put(parameterNames[i], json);
            }
        }
        String json;
        try {
            json = JSON.toJSONString(map);
        } catch (Exception e) {
            json = "";
        }
        return json;
    }

    private RedisTemplate redisTemplate() {
        return (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
    }

}
